﻿#region License
/*

	Copyright (C) 2008 Oliver Charles, Daniel Hollander

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
*/
#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Xml;
using Overload.Entities;
using Overload.Phases;

namespace Overload
{
    /// <summary>
    /// A world is a collection of <see cref="ITile"/>, some how assossciated with other tiles.
    /// </summary>
    public interface IWorld
    {
        /// <summary>
        /// Get all tiles adjacent to a given tile
        /// </summary>
        /// <param name="tile">The tile to search around</param>
        /// <returns>An array of tiles of all surrounding tiles</returns>
        ITile[] GetAdjacentTiles(ITile tile);

        /// <summary>
        /// Calculate the distance between 2 tiles
        /// </summary>
        /// <param name="start">The starting tile</param>
        /// <param name="end">The ending tile</param>
        /// <returns>The distance between <paramref name="start"/> and <paramref name="end"/></returns>
        float TileDistance(ITile start, ITile end);
    }

    /// <summary>
    /// The world that the game is played on; a collection of <see cref="Tile"/>s
    /// </summary>
    public sealed class World : Moebius.Components.World, IWorld
    {
        private readonly Tile[][] worldData;
        private readonly Dictionary<Color, TileType> knownTileTypes = new Dictionary<Color, TileType>();
        private TileType defaultTileType;

        #region Properties

        private readonly int width;
        private readonly int height;
        private readonly Terrain terrain;
        private PlacementPhase currentPhase;

        /// <summary>
        /// The amount of tiles horizontally in this <see cref="World"/>
        /// </summary>
        public int Width
        {
            get { return width; }
        }

        /// <summary>
        /// The amount of tiles vertically in this <see cref="World"/>
        /// </summary>
        public int Height
        {
            get { return height; }
        }

        public Terrain Terrain
        {
            get { return terrain; }
        }

        public PlacementPhase CurrentPhase
        {
            get { return currentPhase; }
            set { currentPhase = value; }
        }

        #endregion

        #region Constructors

        public World(int worldWidth, int worldHeight)
        {
            width = worldWidth;
            height = worldHeight;

            worldData = new Tile[worldHeight][];
            for (int y = 0; y < worldHeight; y++)
            {
                worldData[y] = new Tile[worldWidth];
                for (int x = 0; x < worldWidth; x++)
                    worldData[y][x] = new Tile(ArrayToHex(new Point(x, y)));
            }

            terrain = new Terrain(this, 60);
        }

        #endregion
        #region World Loading

        public static World LoadWorld(string tileTypesFileName, string imageFileName)
        {
			if(!File.Exists(tileTypesFileName)) throw new FileNotFoundException(tileTypesFileName);
			if(!File.Exists(imageFileName)) throw new FileNotFoundException(imageFileName);
			
            Bitmap image = new Bitmap(imageFileName);

            World ret = new World(image.Width, image.Height);
            ret.LoadTileTypeDefinitions(tileTypesFileName);

            for (int x = 0; x < ret.width; x++)
                for (int y = 0; y < ret.height; y++)
                {
                    Color tileColor = image.GetPixel(x, y);

                    TileType tt = ret.defaultTileType;
                    if (ret.knownTileTypes.ContainsKey(tileColor))
                        tt = ret.knownTileTypes[tileColor];

                    ret.GetTileFromArray(x, y).TileType = tt;
                }


            return ret;
        }

        #endregion
        #region Initialization

        public void LoadTileTypeDefinitions(string tileTypeFile)
        {
            XmlDocument document = new XmlDocument();
            document.Load(tileTypeFile);

            XmlNode root = document.SelectSingleNode("tileTypes");

            XmlNode defaultTileTypeNode = root.SelectSingleNode("default");
            AddTileType(defaultTileTypeNode, true);

            XmlNodeList tileTypes = root.SelectNodes("tileType");

            foreach(XmlNode tileTypeNode in tileTypes)
                AddTileType(tileTypeNode, false);
        }
        private void AddTileType(XmlNode tileTypeNode, bool defaultTile)
        {
            string image = tileTypeNode.Attributes["image"].Value.Trim();
            int r, g, b;
            r = g = b = 0;
            int cost = 1;

            if (!defaultTile)
            {
                r = int.Parse(tileTypeNode.Attributes["r"].Value.Trim());
                g = int.Parse(tileTypeNode.Attributes["g"].Value.Trim());
                b = int.Parse(tileTypeNode.Attributes["b"].Value.Trim());
                cost = int.Parse(tileTypeNode.Attributes["cost"].Value.Trim());
            }

            Color tileColor = Color.FromArgb(r, g, b);
            TileType tileType = new TileType(cost, image);

            if (defaultTile)
                defaultTileType = tileType;
            else
                knownTileTypes.Add(tileColor, tileType);
        }

        public override void Initialize(Moebius.Core.ResourceFactory factory)
        {
            base.Initialize(factory);

            currentPhase = new PlacementPhase(this);
        }

        #endregion
        
        #region Space Conversion

        public static Point ArrayToHex(int arrayX, int arrayY)
        {
            return new Point((int)-Math.Ceiling(arrayX / 2.0) - arrayY,
                             (int)Math.Floor(arrayX / 2.0) - arrayY);
        }

        /// <summary>
        /// Convert a point from array-space coordinates to hex-space coordinates
        /// </summary>
        /// <param name="arrayPoint">A Point containing the array-space coordinates</param>
        /// <returns>A new Point containing the same coordinates in hex-space</returns>
        public static Point ArrayToHex(Point arrayPoint)
        {
            return ArrayToHex(arrayPoint.X, arrayPoint.Y);
        }

        /// <summary>
        /// Convert a point from hex-space coordinates to array-space
        /// </summary>
        /// <param name="hexCoordinates">A point containing the hex-space coordinates</param>
        /// <returns>A new Point containing the array-space coordinates</returns>
        public static Point HexToArray(Point hexCoordinates)
        {
            return HexToArray(hexCoordinates.X, hexCoordinates.Y);
        }

        /// <summary>
        /// Convert a point from hex-space coordinates to array-space
        /// </summary>
        public static Point HexToArray(int hexX, int hexY)
        {
            return new Point(hexY - hexX,
                             (int)Math.Floor((-hexX - hexY) / 2.0));
        }

        #endregion
        #region Distance Between 2 Tiles

        public float TileDistance(ITile start, ITile end)
        {
            int dX = end.WorldCoordinates.X - start.WorldCoordinates.X;
            int dY = end.WorldCoordinates.Y - start.WorldCoordinates.Y;

            return (Math.Abs(dX) + Math.Abs(dY) + Math.Abs(dX - dY))/2;
        }

        #endregion
        #region Get Adjacent Tiles

        /// <summary>
        /// Get all tiles adjacent to a <see cref="Tile"/>
        /// </summary>
        /// <param name="hexCoordinates">The hex-space coordinates of the <see cref="Tile"/>.</param>
        /// <returns>An array of <see cref="Tile"/>s that are adjacent to the tile at <paramref name="hexCoordinates"/></returns>
        public ITile[] GetAdjacentTiles(Point hexCoordinates)
        {
            if (!WithinWorldBounds(hexCoordinates))
                throw new InvalidLocationException(hexCoordinates);

            List<Tile> adjacentTiles = new List<Tile>();

            Point[] adjacentPoints = new Point[]
                {
                    new Point(hexCoordinates.X + 1, hexCoordinates.Y),
                    new Point(hexCoordinates.X - 1, hexCoordinates.Y),
                    new Point(hexCoordinates.X, hexCoordinates.Y - 1),
                    new Point(hexCoordinates.X, hexCoordinates.Y + 1),
                    new Point(hexCoordinates.X + 1, hexCoordinates.Y + 1),
                    new Point(hexCoordinates.X - 1, hexCoordinates.Y - 1), 
                };

            foreach(Point p in adjacentPoints)
                if(WithinWorldBounds(p))
                    adjacentTiles.Add(GetTile(p));

            return adjacentTiles.ToArray();
        }

        #endregion
        #region Fetching Tiles

        /// <summary>
        /// Get a tile from the world array
        /// </summary>
        /// <param name="x">The x location of the tile</param>
        /// <param name="y">The y location of the tile</param>
        /// <returns>The tile at the location (<paramref name="x"/>, <paramref name="y"/>)</returns>
        public Tile GetTileFromArray(int x, int y)
        {
            if (!WithinArrayBounds(x, y))
                throw new ArgumentOutOfRangeException();

            return worldData[y][x];
        }

        /// <summary>
        /// Get a tile from the world array
        /// </summary>
        /// <param name="arrayCoordinates">The coordinates of the position of the tile, in the world array</param>
        /// <returns>The tile at the position specified in <paramref name="arrayCoordinates"/></returns>
        public Tile GetTileFromArray(Point arrayCoordinates)
        {
            return GetTileFromArray(arrayCoordinates.X, arrayCoordinates.Y);
        }

        /// <summary>
        /// Get a tile, using world coordinates
        /// </summary>
        /// <param name="worldCoordinates">The world coordinates of the tile</param>
        /// <returns>The tile at <paramref name="worldCoordinates"/></returns>
        public Tile GetTile(Point worldCoordinates)
        {
            Point arrayCoords = HexToArray(worldCoordinates);
            return GetTileFromArray(arrayCoords);
        }

        /// <summary>
        /// Get a tile, using world coordinates
        /// </summary>
        /// <param name="worldX">The x location of the tile in the world</param>
        /// <param name="worldY">The y location of the tile in the world</param>
        /// <returns>The tile at (<paramref name="worldX"/>, <paramref name="worldY"/>)</returns>
        public Tile GetTile(int worldX, int worldY)
        {
            return GetTile(new Point(worldX, worldY));
        }

        /// <summary>
        /// Fetch all tiles that are adjacent to a given <see cref="ITile"/>.
        /// </summary>
        /// <param name="tile">The <see cref="ITile"/> to search around.</param>
        /// <returns>
        /// An array of <see cref="ITile"/>s of all <see cref="ITile"/>s surrounding <paramref name="tile"/>
        /// </returns>
        public ITile[] GetAdjacentTiles(ITile tile)
        {
            return GetAdjacentTiles(tile.WorldCoordinates);
        }

        #endregion
        #region Within World Bounds

        /// <summary>
        /// Check that a point is within the bounds of the world.
        /// </summary>
        /// <param name="coords">The world coordinates to check</param>
        /// <returns>True if the coords are within world bounds; false otherwise</returns>
        public bool WithinWorldBounds(Point coords)
        {
            return WithinArrayBounds(HexToArray(coords));
        }

        #endregion

        #region Private Helpers

        private bool WithinArrayBounds(int x, int y)
        {
            return (x >= 0 && x < Width) && (y >= 0 && y < Height);
        }
        private bool WithinArrayBounds(Point coords)
        {
            return WithinArrayBounds(coords.X, coords.Y);
        }

        #endregion
    }
}